"""

.. module:: manager
.. moduleauthor:: @author: 

The purpose of this module is ... 
   
.. seealso:: modules :mod:`<module>`

*Constants*:
.. data:: constant_1
        
    constant_1 is used for ......
        
.. data:: constant_2
        
    constant_2 is used for ......
"""

# Imports
#
from xml.etree import ElementTree
from zipfile import ZipFile, BadZipfile
import base64
import io
import logging
import sys
import urllib

# Constants
#
PACKAGE_URI = 'http://yarra-project.com/terms/base/package/'
PACKAGE_FILENAME = PACKAGE_URI + 'filename'
PACKAGE_PACKAGENAME = PACKAGE_URI + 'packagename'
PY_EXTENSION = '.py'

# Classes
#

class XMLFormatException(Exception):
    """The purpose of this class is ... 

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    # Class constants
    #
    
    pass

class BadYarraPackageException(Exception):
    """The purpose of this class is ... 

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    # Class constants
    #
    
    pass

class PackageManager(object):
    """The purpose of this class is ... 

    *Usage examples*::

        example code

    *Common Arguments for Class methods*:
        <arguments>
    
    *Audience*:
        <audience>
    
    *Constants*:
        .. data:: constant_1
        
            constant_1 is used for ......
        
        .. data:: constant_2
        
            constant_2 is used for ......
    """
    
    # Class constants
    #
    
    def __init__(self, serialised_object=None, strict=False, namespaces=None):
        # _id,  _metadata, _strict and _ns are private instance variables.
        # see http://docs.python.org/3.1/tutorial/classes.html#private-variables
        #
        # Parameters:
        #     <param 1> :
        #     ...
        #
        
        self.logger = logging.getLogger('yarra.package.manager.PackageManager')
    
    def install_distribution(self, ds, file_repository_location, distribution_name):
        """This function installs the distribution 'distribution_name' at 
        the distribution repository location specified by 'file_repository_location'.
        
        :param ds: datastore.database.Datastore - DataStore instance.
        :param file_repository_location: string
        :param distribution_name: string
        """
        
        if ('datastore' in sys.modules):
            database = sys.modules['datastore.database']
            resource = sys.modules['datastore.resource']
        packages = self.get_distribution(file_repository_location, distribution_name)
        for package in packages:
            self.install_package(package, ds, True)

    def update_package(self, package_resource, ds):
        """This function updates a package in the datastore.
        
        :param package_resource: datastore.resource.Resource - Package resource.
        :param ds: datastore.database.Datastore - DataStore instance.
        """

        binary_contents = base64.b64decode(package_resource.bin)
        self.logger.log(logging.DEBUG, 'Decoded binary contents')
        package = self.get_package(package_binary_contents=binary_contents)
        self.logger.log(logging.DEBUG, 'Got package')
        self.install_package(package, ds, False)
        self.logger.log(logging.DEBUG, 'Installed package')
    
    def install_package(self, package, ds, installing_distribution):
        """This function installs a package resource. Installing a package is 
        just adding the package's resources to the datastore. Previous 
        resources from the package are removed. First time installs (i.e. 
        Installing the distribution), this method will not have access to a 
        spaces_context, so there is no need to check if there are existing 
        versions of the package, as the first time install will be a fresh 
        install with NO packages already installed in the datastore. When 
        updating individual packages, we will have access to the spaces_context, 
        as the runtime will already be installed. Hence we can check
        for existing versions of the package and delete those resouces.
        
        :param package: package.pkge.Package - Package to be installed.
        :param ds: datastore.database.Datastore - Datastore instance.
        :param installing_distribution: Boolean - True if we are installing a distribution.
        """
        
        from datastore.database import Uri, NotFoundException
        uri_package_packagename = Uri(PACKAGE_PACKAGENAME)
        package_name = None
        
        new_package_resources = []
        # collect all the new resources for the package
        for rs in package.contents:
            if not package_name:
                package_name = rs.get_single_metadata(uri_package_packagename)
            new_package_resources.append(rs)        
        
        existing_package_resources = []
        if (not installing_distribution):
            try:
                # collect all the existing resources for the package
                existing_package_resources = ds.find([(uri_package_packagename, package_name)])
            except NotFoundException:
                # Not a problem if we can't find exisitng resources for the package. This just
                # means this package has not been installed
                pass
        
        # delete the old resources and add the new resources.
        ds.delete_put(existing_package_resources, new_package_resources)
        self.logger.log(logging.INFO, "\tinstalled %s package" % package_name)
        
    def load_active_packages(self, ds):
        """This function loads the active modules.
        
        :param ds: datastore.database.Datastore - Datastore instance.
        """
        
        resources = ds.find([('active', True)])

    def get_distribution(self, file_repository_location, distribution_name, specific_packages=None):
        """This function returns the packages in the distribution specified by 
        'distribution_name' and 'file_repository_location'. Optionally, 
        'specific_packages' is a list to filter the packages to return 
        from the distribution.
        
        :param file_repository_location: string - location of distribution file.
        :param distribution_name: string - name of distribution file.
        :param specific_packages: List - list of package names to get from distribution.
        :rtype: List
        """
        
        # download the distribution xml
        packages = []
        self.logger.log(logging.INFO, 'Downloading distribution: {0}'.format(distribution_name))
        url = file_repository_location + distribution_name + ".xml"            
        distribution_file = self.download_file(url)
        distribution_xml = ElementTree.XML(distribution_file.read())
        packages_node = distribution_xml.findall('.//package')
        for package_node in packages_node:
            package_name = package_node.find('./name').text
            if (specific_packages is None) or (package_name in specific_packages):
                package_location = package_node.find('./location').text
                package = self.get_package(package_url=package_location)
                packages.append(package)
        return packages

    def download_file(self, url):
        """This function returns the file located at 'url'.
        
        :param url: string - the file to open.
        :rtype: file handle
        """
        
        return urllib.urlopen(url)

    def get_package(self, package_url=None, package_binary_contents=None):
        """This function returns the contents of the package as a Package.
        
        :param package_url: string - representation of a url.
        :param package_binary_contents: string - binary contents
        :rtype: Package
        """
        
        if (package_url):
            # download the package
            file = self.download_file(package_url)
            zip_data = io.BytesIO(file.read())
            file.close()
        else:
            zip_data = io.BytesIO(package_binary_contents)
        try:
            package_zip = ZipFile(zip_data)
        except BadZipfile:
            raise BadYarraPackageException() 
        
        try:
            # get the configuration resource
            self.get_resource_from_zip(package_zip, 'configuration.xml', encoded=False)
        except KeyError:
            raise BadYarraPackageException()
    
        from package.pkge import Package
        package = Package()
        
        # grab everything from the yarra package for now.
        for file_name in package_zip.namelist():
            resource = self.get_resource_from_zip(package_zip, file_name, file_name.find(PY_EXTENSION) > -1 )       
            package.contents.append(resource)
            
        return package
    
    def get_resource_from_zip(self, zip, resource_name, encoded):
        """This function returns the contents of the file 'resource_name' - 
        a serialised Resource, within the ZIP file 'zip', as a Resource. If 
        'encoded' is True then the contents of 'resource_name' are base64 
        decoded.
        
        :param zip: ZipFile - The ZIP archive file containing the Resource.
        :param resource_name: string - name of the Resource to retrieve.
        :param encoded: Boolean - True if we should base64-decode the contents
        of the Resource.
        :rtype: Resoure
        """
        
        file = zip.open(resource_name)
        # fix of what might be a bug in the ZipFile library where one of the instance attributes is set to a long instead of a int
        file.bytes_read = 0
        serialised_resource = file.read()
        file.close()
        # force closing of stream by setting the file to None. This causes the destructor to run 
        file = None
        rs = sys.modules['datastore.resource']
        resource = rs.Resource(serialised_resource)
        if (encoded is True):
            resource.content = base64.decodestring(resource.content)
        return resource